package tv.geir.slackspace.analyzer;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.objectweb.asm.ClassReader;

import tv.geir.slackspace.code.BytecodeRetrievalException;
import tv.geir.slackspace.code.CachingBytecodeFinder;
import tv.geir.slackspace.code.ClassDefinition;
import tv.geir.slackspace.code.PackageConfiguration;


/**
 * Used to locate a class' dependencies on other classes. This includes
 * all classes and interfaces needed at runtime including those
 * not exposed through reflection ( by analyzing the underlying
 * bytecode). Note: analysis is not recursive at the moment, so transient
 * dependencies will not be included at this stage. 
 * 
 * @author Geir Gullestad Pettersen
 *
 */
public class BytecodeDependencyAnalyzer {

	/** Used to get lookup bytecode */
	private CachingBytecodeFinder bytecodeFinder = null;
	
	/**
	 * contains a set of classdefiniton that a classdefinition depends on.
	 */
	private Map<ClassDefinition, Set<ClassDefinition>> dependencyMap;
	
	private static Log log = LogFactory.getLog( BytecodeDependencyAnalyzer.class );
	
	/**
	 * Configuration, which packages to include or exclude
	 */
	private PackageConfiguration config;
	
	
	/**
	 * Creates an instance of BytecodeDependencyAnalyzer
	 */
	public BytecodeDependencyAnalyzer() {
		
		dependencyMap = new LinkedHashMap<ClassDefinition,Set<ClassDefinition>>();
		bytecodeFinder = CachingBytecodeFinder.getInstance();
	}
	

	/**
	 * Returns a set of class definitions <code>clazz</code> depends on at runtime limited
	 * to classes belonging to any of the packages in the array <code>packages</code>.
	 * <p>
	 * Note! Scanning algorithm is not recursive and it only searches for classes
	 * referenced in the bytecode of <code>clazz</code>. To find indirect dependencies
	 * you will have to analyze the the set of classdefinitions as well.
	 * 
	 * @param clazz name of the class to analyze
	 * @return a list of dependencies, or <code>null</code> if <code>clazz</code> is not 
	 * included in <code>config</code>.
	 */
	public Set<ClassDefinition> findDependencies( String clazz , String... packages) {
		if( config.includeClass( clazz )) {
			try {
				if( clazz.indexOf( '/' ) > 0 ) {
					clazz = clazz.replace('.', '/');
				}
				
				// get bytecode				
				ClassDefinition def = bytecodeFinder.findClassDefinition( clazz );
				
				// check for dependencies in cache
				Set<ClassDefinition> dependencies = dependencyMap.get( def );
				
				// if not in cache, try to resolve dependencies
				if( dependencies == null ) {
					dependencies = resolveDependencies(def);
					
					// add to cache
					dependencyMap.put( def, dependencies );
				} 
					
				if( log.isDebugEnabled() ) {
					log.debug("Dependencies for " + clazz + " are : " + dependencies);
				}
				return dependencies;
			} catch (BytecodeRetrievalException bre ) {
				if( log.isDebugEnabled() ) {
					log.info("Error retrieving bytecode, returning null", bre );
				} else if( log.isInfoEnabled() ) {
					log.info("Error retrieving bytecode, returning null: " + bre.getMessage() );
				}
				return null;
			}
		}
		
		// if class should not be included, return null
		return null;
	}

	/**
	 * Resolves dependencies for the class represented by <code>def</code>
	 * and returns those dependencies in form of a <code>Set<ClassDefinition></code>.
	 * Dependencies are then stored in the local cache so secondary 
	 * lookups will go faster.
	 * @param def 
	 * @return
	 */
	private Set<ClassDefinition> resolveDependencies(ClassDefinition def) {
		Set<ClassDefinition> dependsOn;
		dependsOn = dependsOn( def.getDefinition() );
		
		// if resolved, add to cache.
		if( dependsOn != null ) {
			// add self to dependencies
			dependsOn.add( def );
			
			
		}
		return dependsOn;
	}
	
	/**
	 * Analyzes the bytecode and returns a list of class definitions that the
	 * bytecode is dependent on. Excludes classes that are not in <code>config</code>.
	 * 
	 * @param byteCode the bytecode to analyze
	 * @param limitToPackage 
	 * @return
	 */
	private Set<ClassDefinition> dependsOn( byte[] byteCode ){ 
		// find all dependencies for this bytecode
		Set<String> dependingClassNames = dependingClassNames( byteCode );		
		
		
		// create class definitions from class names
		Set<ClassDefinition> dependencies = new HashSet<ClassDefinition>();		
		for( String classname : dependingClassNames ) {
			if( config.includeClass( classname )) {
				try {
					dependencies.add( bytecodeFinder.findClassDefinition(classname));
				} catch( BytecodeRetrievalException bnfe ) {
					log.warn( "Bytecode for \'" + classname + "\' not found. Object may not be deserializable at other end", bnfe );
				}
			}	
		}
		
		
		return dependencies;
	}
	
	/**
	 * Returns a list of classes the given bytecode is depending on.
	 * @param byteCode the code to analyze
	 * @param packages a list of packages
	 * @return list of class names
	 */
	private Set<String> dependingClassNames( byte[] byteCode, String... packages ) {
		boolean skipDebug = true;
		ClassVisitorAnalyzer visitor = new ClassVisitorAnalyzer();
		ClassReader reader = new ClassReader( byteCode );		
		reader.accept( visitor, skipDebug );
		
		return visitor.getDependencies();		
	}	

	/**
	 * Scans classpath and code repository for bytecode for given class. 
	 * 
	 * @param className
	 * @return
	 * @throws BytecodeRetrievalException if the bytecode for this class is not found
	 */
	private ClassDefinition getClassDefinition( String classname ) throws BytecodeRetrievalException{
		// first try the cd registry
		return bytecodeFinder.findClassDefinition(classname);
	}
	
	/**
	 * Returns true if a class named <code>className</code> belongs to any of the
	 * packages in the <code>packages</code>-array
	 * @param className
	 * @param packages
	 * @return
	 */
	public boolean classResidesIn ( String className, String... packages ) {
		for( String p : packages ) {
			if( className.startsWith( p )) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Returns a copy of <code>classes</code> where only classes belonging to a package
	 * in the array <code>packages</code> are included.
	 * @param classes set of class names
	 * @param packageNames array of package names
	 * @return
	 */
	private Set<String> limitClassNamesToPackage( Set<String> classes, String... packageNames ) {
		Set<String> limitedClasses = new HashSet<String>();
		for( String c : classes ) {
			if( classResidesIn( c, packageNames )) {
				limitedClasses.add( c );
			}
		}
			
		return limitedClasses;
	}

	public void setConfig(PackageConfiguration config) {
		this.config = config;
	}
	
}
